#include "Subsystems/BlockBehaviors/SubsystemBowBlockBehavior.h"
#include "Subsystems/SubsystemTimeOfDay.h"
#include "Subsystems/SubsystemProjectiles.h"

#include "Blocks/BowBlock.h"
#include "Blocks/ArrowBlock.h"
#include "Managers/BlocksManager.h"
#include "Components/ComponentMiner.h"
#include "Components/ComponentCreature.h"
#include "Components/ComponentCreatureModel.h"
#include "Components/ComponentBody.h"
#include "Components/ComponentPlayer.h"
#include "Components/ComponentLevel.h"
#include "GameRandom.hpp"

namespace PocketSurvival
{

    static bool supportedArrowTypes[10] =
        {
            false, // None
            true,  // WoodenArrow
            true,  // StoneArrow
            true,  // IronArrow
            true,  // DiamondArrow
            true,  // FireArrow
            false, // IronBolt
            false, // DiamondBolt
            false, // ExplosiveBolt
            true   // CopperArrow
    };

    SubsystemBowBlockBehavior::SubsystemBowBlockBehavior()
    {
    }

    void SubsystemBowBlockBehavior::setAimTime(ComponentMiner *componentMiner, double time)
    {
        std::lock_guard<std::mutex> lk(m_mapMutex);
        aimStartTimes[componentMiner] = time;
    }

    void SubsystemBowBlockBehavior::removeAimTime(ComponentMiner *componentMiner)
    {
        std::lock_guard<std::mutex> lk(m_mapMutex);
        auto it = aimStartTimes.find(componentMiner);
        if (it != aimStartTimes.end())
        {
            aimStartTimes.erase(it);
        }
    }

    double SubsystemBowBlockBehavior::getAimTime(ComponentMiner *componentMiner)
    {
        std::lock_guard<std::mutex> lk(m_mapMutex);
        auto it = aimStartTimes.find(componentMiner);
        if (it != aimStartTimes.end())
        {
            return it->second;
        }
        return -1;
    }

    void SubsystemBowBlockBehavior::update(float dt)
    {
        if (m_timeOfDay->periodicGameTimeEvent(15.0, 0.0) == true)
        {
            std::lock_guard<std::mutex> lk(m_mapMutex);
            bool needChange = false;
            double gameTime = m_timeOfDay->getGameTime();
            for (auto it = aimStartTimes.begin(); it != aimStartTimes.end(); ++it)
            {
                if (gameTime - it->second > 15.0)
                {
                    needChange = true;
                    break;
                }
            }
            if (needChange == true)
            {
                std::unordered_map<ComponentMiner *, double> aimTimeMap;
                for (auto it = aimStartTimes.begin(); it != aimStartTimes.end(); ++it)
                {
                    if (gameTime - it->second < 15.0)
                    {
                        aimTimeMap[it->first] = it->second;
                    }
                }
                aimStartTimes.swap(aimTimeMap);
            }
        }
    }

    bool SubsystemBowBlockBehavior::onAim(const Ray3 &aim, ComponentMiner *componentMiner, AimState state)
    {
        IInventory *inventory = componentMiner->inventory;
        if (inventory != nullptr)
        {
            int32_t activeSlotIndex = inventory->getActiveSlotIndex();
            if (activeSlotIndex >= 0)
            {
                int32_t slotValue = inventory->getSlotValue(activeSlotIndex);
                int32_t slotCount = inventory->getSlotCount(activeSlotIndex);
                int32_t num = Terrain::ExtractContents(slotValue);
                int32_t data = Terrain::ExtractData(slotValue);
                if (num == BowBlock::Index() && slotCount > 0)
                {
                    double value = getAimTime(componentMiner);
                    if (value < 0.0)
                    {
                        value = m_timeOfDay->getGameTime();
                        setAimTime(componentMiner, value);
                    }

                    float num2 = (float)(m_timeOfDay->getGameTime() - value);
                    float num3 = (float)MathUtils::Remainder(m_timeOfDay->getGameTime(), 1000.0);

                    switch (state)
                    {
                    case AimState::InProgress:
                    {
                        if (num2 >= 9.0f)
                        {
                            return true;
                        }
                        if (m_timeOfDay->periodicGameTimeEvent(0.1, 0.0))
                        {
                            int32_t draw2 = MathUtils::Min<int32_t>(BowBlock::GetDraw(data) + 1, 15);
                            inventory->removeSlotItems(activeSlotIndex, 1);
                            inventory->addSlotItems(activeSlotIndex, Terrain::MakeBlockValue(num, 0, BowBlock::SetDraw(data, draw2)), 1);
                        }
                        break;
                    }
                    case AimState::Cancelled:
                    {
                        inventory->removeSlotItems(activeSlotIndex, 1);
                        inventory->addSlotItems(activeSlotIndex, Terrain::MakeBlockValue(num, 0, BowBlock::SetDraw(data, 0)), 1);
                        removeAimTime(componentMiner);
                    }
                    break;
                    case AimState::Completed:
                    {
                        int32_t draw = BowBlock::GetDraw(data);
                        ArrowType arrowType = BowBlock::GetArrowType(data);
                        if (arrowType != ArrowType::None)
                        {
                            Vector3 vector = componentMiner->componentCreature->componentCreatureModel->getEyePosition() + 
                                             componentMiner->componentCreature->componentBody->getMatrix().getRight() * 0.3f - 
                                             componentMiner->componentCreature->componentBody->getMatrix().getUp() * 0.2f;

                            Vector3 vector2 = Vector3::Normalize(vector + aim.Direction * 10.0f - vector);
                            float num4 = MathUtils::Lerp(0.0f, 28.0f, MathUtils::Pow(draw / 15.0f, 0.75f));
                            num4 *= 0.5f * (componentMiner->componentPlayer->componentLevel->getStrengthFactor() - 1.0f) + 1.0f;

                            Vector3 vector3 = FastVector3::Zero;
                            if (arrowType == ArrowType::WoodenArrow)
                            {
                                vector3.posX = 0.025f;
                                vector3.posY = 0.025f;
                                vector3.posZ = 0.025f;
                            }
                            if (arrowType == ArrowType::StoneArrow)
                            {
                                vector3.posX = 0.01f;
                                vector3.posY = 0.01f;
                                vector3.posZ = 0.01f;
                            }

                            int32_t value2 = Terrain::MakeBlockValue(ArrowBlock::Index(), 0, ArrowBlock::SetArrowType(0, arrowType));
                            Vector3 vector4 = Vector3::Normalize(Vector3::Cross(vector2, FastVector3::UnitY));
                            Vector3 v2 = Vector3::Normalize(Vector3::Cross(vector2, vector4));

                            Vector3 v3 = GameSingleton::gameRandom->getFloat(0.0f - vector3.posX, vector3.posX) * vector4 + 
                                         GameSingleton::gameRandom->getFloat(0.0f - vector3.posY, vector3.posY) * v2 + 
                                         GameSingleton::gameRandom->getFloat(0.0f - vector3.posZ, vector3.posZ) * vector2;

                            if (m_projectiles->fireProjectile(value2, vector, (vector2 + v3) * num4, FastVector3::Zero, componentMiner->componentCreature) != nullptr)
                            {
                                data = BowBlock::SetArrowType(data, ArrowType::None);
                            }
                        }

                        inventory->removeSlotItems(activeSlotIndex, 1);
                        int32_t value3 = Terrain::MakeBlockValue(num, 0, BowBlock::SetDraw(data, 0));
                        inventory->addSlotItems(activeSlotIndex, value3, 1);
                        int32_t damageCount = 0;
                        if (draw >= 15)
                        {
                            damageCount = 2;
                        }
                        else if (draw >= 4)
                        {
                            damageCount = 1;
                        }
                        componentMiner->damageActiveTool(damageCount);
                        removeAimTime(componentMiner);

                        return true;
                    }
                    }
                }
            }
        }
        return false;
    }

    uint32_t SubsystemBowBlockBehavior::getProcessInventoryItemCapacity(IInventory *inventory, uint32_t slotIndex, int32_t cellValue)
    {
        int32_t num = Terrain::ExtractContents(cellValue);
        ArrowType arrowType = ArrowBlock::GetArrowType(Terrain::ExtractData(cellValue));
        if (num == ArrowBlock::Index() && supportedArrowTypes[(uint8_t)arrowType] == true)
        {
            if (BowBlock::GetArrowType(Terrain::ExtractData(inventory->getSlotValue(slotIndex))) == ArrowType::None)
            {
                return 1;
            }
            return 0;
        }
        return 0;
    }

    void SubsystemBowBlockBehavior::processInventoryItem(IInventory *inventory, uint32_t slotIndex, int32_t cellValue, uint32_t count, uint32_t processCount, int32_t &outProcessedValue, uint32_t &outProcessedCount)
    {
        if (processCount == 1)
        {
            ArrowType arrowType = ArrowBlock::GetArrowType(Terrain::ExtractData(cellValue));
            int32_t slotValue = inventory->getSlotValue(slotIndex);
            int32_t data = Terrain::ExtractData(slotValue);
            outProcessedValue = 0;
            outProcessedCount = 0;
            inventory->removeSlotItems(slotIndex, 1);
            inventory->addSlotItems(slotIndex, Terrain::MakeBlockValue(BowBlock::Index(), 0, BowBlock::SetArrowType(data, arrowType)), 1);
        }
        else
        {
            outProcessedValue = cellValue;
            outProcessedCount = count;
        }
    }

    void SubsystemBowBlockBehavior::load(const nlohmann::json &json)
    {
        m_timeOfDay = GameSingleton::singletonPtr->getSingleton<SubsystemTimeOfDay>();
        m_projectiles = GameSingleton::singletonPtr->getSingleton<SubsystemProjectiles>();
    }

    static const std::string subsystemName = "BowBlockBehavior";
    const std::string &SubsystemBowBlockBehavior::getName() const
    {
        return subsystemName;
    }
    SubUpdateType SubsystemBowBlockBehavior::getUpdateType()
    {
        return SubUpdateType::MultiThreadUpdate;
    }

}